﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Tinter.Common.TypeMappers;

namespace Tinter.Common.Pagination
{
    /// <summary>
    /// Represents a generic list with pagination information.
    /// </summary>
    /// <typeparam name="T">The type of item.</typeparam>
    [DataContract]
    public class PagedItems<T> : IPageInfo
    {
        #region Fields

        private readonly List<T> list = new List<T>();
        private int pageNumber;
        private int pageSize;
        private int totalItemCount;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates an instance of PagedList.
        /// </summary>
        /// <param name="source">Source sequence contains all items to be paged.</param>
        /// <param name="pageNumber">One-based page number.</param>
        /// <param name="pageSize">Size of each page.</param>
        public PagedItems(IEnumerable<T> source, int pageNumber, int pageSize)
        {
            if (source == null) throw new ArgumentNullException(MethodBase.GetCurrentMethod().GetParameters()[0].Name);
            if (pageSize <= 0) throw new ArgumentOutOfRangeException(MethodBase.GetCurrentMethod().GetParameters()[2].Name);

            this.pageNumber = Math.Max(pageNumber, 1);
            this.pageSize = pageSize;
            this.totalItemCount = source.Count();

            this.list.AddRange(source.Skip((this.PageNumber - 1) * this.PageSize).Take(this.PageSize));
        }

        /// <summary>
        /// Creates an instance of PagedList based on IQueryable source.
        /// </summary>
        /// <remarks>
        /// Explicitly specify a constructor for IQueryable to 
        /// avoid loading all items from persistence when counting the total number.
        /// </remarks>
        /// <param name="source">Source sequence contains all items to be paged.</param>
        /// <param name="pageNumber">One-based page number.</param>
        /// <param name="pageSize">Size of each page.</param>
        public PagedItems(IQueryable<T> source, int pageNumber, int pageSize)
        {
            if (source == null) throw new ArgumentNullException(MethodBase.GetCurrentMethod().GetParameters()[0].Name);
            if (pageSize <= 0) throw new ArgumentOutOfRangeException(MethodBase.GetCurrentMethod().GetParameters()[2].Name);

            this.pageNumber = Math.Max(pageNumber, 1);
            this.pageSize = pageSize;
            this.totalItemCount = source.Count();

            this.list.AddRange(source.Skip((this.PageNumber - 1) * this.PageSize).Take(this.PageSize));
        }

        /// <summary>
        /// Creates an instance of PagedList.
        /// </summary>
        /// <param name="totalItemCount">Item count in all pages.</param>
        /// <param name="pageNumber">One-based page number.</param>
        /// <param name="pageSize">Size of each page.</param>
        /// <param name="items">Item in current page.</param>
        public PagedItems(int totalItemCount, int pageNumber, int pageSize, IEnumerable<T> items)
        {
            if (totalItemCount < 0) throw new ArgumentOutOfRangeException(MethodBase.GetCurrentMethod().GetParameters()[0].Name);
            if (pageSize <= 0) throw new ArgumentOutOfRangeException(MethodBase.GetCurrentMethod().GetParameters()[2].Name);

            this.pageNumber = Math.Max(pageNumber, 1);
            this.pageSize = pageSize;
            this.totalItemCount = totalItemCount;

            this.list.AddRange(items);
        }

        #endregion

        #region IPageInfo Members

        /// <summary>
        /// <see cref="IPageInfo"/>
        /// </summary>
        public int ItemCount
        {
            get { return this.list.Count; }
        }

        /// <summary>
        /// <see cref="IPageInfo"/>
        /// </summary>
        [DataMember]
        public int PageNumber
        {
            get { return this.pageNumber; }
            protected set { this.pageNumber = value; }
        }

        /// <summary>
        /// <see cref="IPageInfo"/>
        /// </summary>
        [DataMember]
        public int PageSize
        {
            get { return this.pageSize; }
            protected set { this.pageSize = value; }
        }

        /// <summary>
        /// <see cref="IPageInfo"/>
        /// </summary>
        [DataMember]
        public int TotalItemCount
        {
            get { return this.totalItemCount; }
            protected set { this.totalItemCount = value; }
        }

        /// <summary>
        /// <see cref="IPageInfo"/>
        /// </summary>
        public int TotalPageCount
        {
            get
            {
                int mod = this.totalItemCount % this.pageSize;
                int count = this.totalItemCount / this.pageSize;

                return mod == 0 ? count : count + 1;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Maps items to target type.
        /// </summary>
        /// <typeparam name="TTarget">Target item type.</typeparam>
        /// <param name="mapper">To map from current item type to target.</param>
        /// <returns>A PagedList with item of target type.</returns>
        public PagedItems<TTarget> MapTo<TTarget>(ITypeMapper mapper)
        {
            List<TTarget> target = mapper.Map<PagedItems<T>, List<TTarget>>(this);

            return new PagedItems<TTarget>(this.totalItemCount, this.pageNumber, this.pageSize, target);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Item list.
        /// </summary>
        [DataMember]
        public T[] Items
        {
            get { return this.list.ToArray(); }
            protected set 
            {
                this.list.Clear();

                if (value != null)
                {
                    this.list.AddRange(value);
                }
            }
        }

        #endregion
    }

    /// <summary>
    /// Contains extenstions to use PagedList.
    /// </summary>
    public static class PagedListExtensions
    {
        public static PagedItems<T> ToPagedList<T>(this IQueryable<T> me, int pageNumber, int pageSize)
        {
            return new PagedItems<T>(me, pageNumber, pageSize);
        }

        public static PagedItems<T> ToPagedList<T>(this IEnumerable<T> me, int pageNumber, int pageSize)
        {
            return new PagedItems<T>(me, pageNumber, pageSize);
        }
    }
}
